Die Boost C++ Bibliotheken


Kapitel 4: Ereignisbehandlung


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


4.1 Allgemeines

Viele Entwickler dürften bei Ereignisbehandlung automatisch an grafische Benutzeroberflächen denken: Dort werden Ereignisse häufig derart eingesetzt, dass zum Beispiel bei einem Klick auf eine Schaltfläche eine Funktion aufgerufen wird, die mit der Schaltfläche verknüpft ist. Der Klick auf die Schaltfläche stellt ein Ereignis dar, und die Funktion, die daraufhin ausgeführt wird, ist der Ereignisverarbeiter.

Dieses Muster kann nicht nur bei grafischen Benutzeroberflächen eingesetzt werden, sondern natürlich auch in anderen Fällen. So kann ein beliebiges Objekt Funktionen aufrufen, wenn ein bestimmtes Ereignis eintritt und etwas passiert, auf das reagiert werden soll. Die Bibliothek Boost.Signals, die in diesem Kapitel vorgestellt wird, macht es einfach, dieses Muster in C++ einzusetzen.

Genaugenommen können Sie auch die Bibliothek Boost.Function zur Ereignisbehandlung verwenden. Ein entscheidender Unterschied zwischen Boost.Function und Boost.Signals ist, dass bei Boost.Function nur eine Funktion aufgerufen werden kann und Boost.Signals die Verknüpfung eines Ereignisses mit beliebig vielen Funktionen erlaubt. Boost.Signals unterstützt eine ereignisorientierte Programmierung wesentlich besser und sollte daher die erste Wahl sein, wenn Ereignisse verarbeitet werden müssen.


4.2 Signale

Der Name der Bibliothek, die in diesem Kapitel vorgestellt wird, hat nichts mit Ereignissen zu tun, sondern mit Signalen. Boost.Signals legt den Schwerpunkt auf Signale, da die Bibliothek das sogenannte Signal-Slot-Konzept umsetzt, in dem nicht von Ereignissen, sondern von Signalen die Rede ist. Dieses Konzept sieht vor, dass beim Aussenden von Signalen die mit den entsprechenden Signalen verknüpften Funktionen - Slots genannt - ausgeführt werden. Wenn Sie sich vorstellen, dass die Signale beim Eintreten bestimmter Ereignisse ausgesandt werden, ist man wieder bei der Ereignisbehandlung. Da aber Signale jederzeit ausgesandt werden können, verzichtet das Signal-Slot-Konzept auf Ereignisse.

Sie werden auch vergebens nach Klassen suchen, die Ereignissen entsprechen. Stattdessen finden Sie eine Klasse boost::signal in Boost.Signals. Diese Klasse ist in der Headerdatei boost/signal.hpp definiert. Dies ist auch die einzige Headerdatei, die Sie kennen und verwenden müssen, weil diese Headerdatei andere zu Boost.Signals gehörende Headerdateien einbindet.

Die Klasse boost::signal befindet sich, wie Sie sehen, im Namensraum boost. Boost.Signals definiert ein paar weitere Klassen, die im Namensraum boost::signals liegen. Da boost::signal die am häufigsten verwendete Klasse von Boost.Signals ist, wurde diese Klasse als einzige direkt in den Namensraum boost gelegt.

#include <boost/signal.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(func); 
  s(); 
} 

Die Klasse boost::signal ist ein Template und erwartet die Signatur der Funktionen, die als Ereignisverarbeiter verwendet werden, als Template-Parameter. Im obigen Beispiel können also mit dem Signal s nur Funktionen verknüpft werden, deren Signatur zu void () passt.

Über die Methode connect() wird die Funktion func() mit dem Signal s verknüpft. Das funktioniert, weil func() kompatibel zur Signatur void () ist. func() ist demnach ein Ereignisverarbeiter, der aufgerufen wird, wenn das Signal s ausgelöst wird.

Um das zu tun, wird s so aufgerufen als würde es sich dabei um eine Funktion handeln. Die Signatur entspricht dabei dem Template-Parameter: So bleiben die runden Klammern leer und es werden keine Parameter übergeben, weil die Signatur void () keine Parameter erwartet.

Durch den Aufruf von s wird also ein Signal ausgelöst, das zum Aufruf der Funktion func() führt, weil diese Funktion mit connect() mit dem Signal verknüpft ist.

Obiges Beispiel kann auch genauso gut mit Boost.Function umgesetzt werden.

#include <boost/function.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::function<void ()> f; 
  f = func; 
  f(); 
} 

Auch im obigen Programm wird bei einem Aufruf von f die verknüpfte Funktion func() aufgerufen. Während mit Boost.Function aber nun Schluss ist, kann mit Boost.Signals mehr angestellt werden. So werden im folgenden Beispiel zwei Funktionen mit einem Signal verknüpft.

#include <boost/signal.hpp> 
#include <iostream> 

void func1() 
{ 
  std::cout << "Hello" << std::flush; 
} 

void func2() 
{ 
  std::cout << ", world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(func1); 
  s.connect(func2); 
  s(); 
} 

Sie können für ein Objekt vom Typ boost::signal die Methode connect() mehrfach aufrufen und so mehrere Funktionen mit einem Signal verknüpfen. Wird das Signal ausgesandt, werden die Funktionen immer genau in der Reihenfolge ausgeführt, in der sie mit connect() mit dem Signal verknüpft wurden.

Die Methode connect() ist überladen, so dass Sie alternativ auch explizit angeben können, in welcher Reihenfolge Funktionen aufgerufen werden sollen. Dazu wird connect() als zusätzlicher Parameter ein int-Wert übergeben.

#include <boost/signal.hpp> 
#include <iostream> 

void func1() 
{ 
  std::cout << "Hello" << std::flush; 
} 

void func2() 
{ 
  std::cout << ", world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(1, func2); 
  s.connect(0, func1); 
  s(); 
} 

Im obigen Programm wird wie in den vorherigen Beispielen zuerst func1() und dann func2() aufgerufen.

Es ist nicht nur möglich, mit connect() eine Funktion mit einem Signal zu verbinden, sondern diese Verbindung auch mit disconnect() wieder zu lösen.

#include <boost/signal.hpp> 
#include <iostream> 

void func1() 
{ 
  std::cout << "Hello" << std::endl; 
} 

void func2() 
{ 
  std::cout << ", world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(func1); 
  s.connect(func2); 
  s.disconnect(func2); 
  s(); 
} 

Obiges Programm gibt lediglich Hello aus, weil vor Aussenden des Signals die Verknüpfung mit der Funktion func2() gelöst wurde.

Neben connect() und disconnect() bietet die Klasse boost::signal nur wenige weitere Methoden an.

#include <boost/signal.hpp> 
#include <iostream> 

void func1() 
{ 
  std::cout << "Hello" << std::flush; 
} 

void func2() 
{ 
  std::cout << ", world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  s.connect(func1); 
  s.connect(func2); 
  std::cout << s.num_slots() << std::endl; 
  if (!s.empty()) 
    s(); 
  s.disconnect_all_slots(); 
} 

num_slots() gibt die Anzahl der verknüpften Funktionen zurück. Ist gar keine Funktion verknüpft, kann anstatt das Ergebnis von num_slots() mit 0 zu vergleichen empty() aufgerufen werden. Die Methode disconnect_all_slots() wiederum macht genau das, was ihr Name ausdrückt: Es werden alle Verknüpfungen gelöst.

Während Sie nun wissen, wie Funktionen mit Signalen verknüpft werden und was passiert, wenn das Signal ausgelöst wird, ist noch offen, was mit Rückgabewerten von Funktionen geschieht. Sehen Sie sich dazu folgendes Beispiel an.

#include <boost/signal.hpp> 
#include <iostream> 

int func1() 
{ 
  return 1; 
} 

int func2() 
{ 
  return 2; 
} 

int main() 
{ 
  boost::signal<int ()> s; 
  s.connect(func1); 
  s.connect(func2); 
  std::cout << s() << std::endl; 
} 

Die beiden Funktionen func1() und func2() besitzen nun einen Rückgabewert vom Typ int. Diese Rückgabewerte werden von s irgendwie verarbeitet und dann auf die Standardausgabe ausgegeben. Was aber passiert hier genau?

Obiges Programm gibt 2 auf die Standardausgabe aus. Die Rückgabewerte werden von s entgegengenommen, bis auf den letzten Rückgabewert aber ignoriert. Standardmäßig wird also lediglich der letzte Rückgabewert aller aufgerufener Funktionen zurückgegeben.

Es ist möglich, ein Signal so anzupassen, dass Rückgabewerte anderweitig verarbeitet werden. Dazu muss der Klasse boost::signal als zweiter Parameter ein Combiner übergeben werden.

#include <boost/signal.hpp> 
#include <iostream> 
#include <algorithm> 

int func1() 
{ 
  return 1; 
} 

int func2() 
{ 
  return 2; 
} 

template <typename T> 
struct min_element 
{ 
  typedef T result_type; 

  template <typename InputIterator> 
  T operator()(InputIterator first, InputIterator last) const 
  { 
    return *std::min_element(first, last); 
  } 
}; 

int main() 
{ 
  boost::signal<int (), min_element<int> > s; 
  s.connect(func1); 
  s.connect(func2); 
  std::cout << s() << std::endl; 
} 

Ein Combiner ist eine Klasse, die den Operator operator()() überlädt. Diese Methode wird automatisch aufgerufen und bekommt zwei Iteratoren übergeben, die auf die in einem Signal gesammelten Rückgabewerte verweisen. In dieser Methode kann nun beispielsweise ein im C++ Standard definierter Algorithmus verwendet werden, um den kleinsten Wert zu ermitteln und zurückzugeben - genau das macht der Algorithmus std::min_element().

Leider ist es nicht möglich, einen Algorithmus wie std::min_element() direkt als Template-Parameter an boost::signal zu übergeben. Die Klasse boost::signal erwartet, dass der Combiner einen Typ result_type definiert. Dieser Typ beschreibt den Typ des Rückgabewertes von operator()(). Fehlt er - wie bei Algorithmen aus dem C++ Standard - meldet der Compiler einen Fehler.

Anstatt Rückgabewerte in einem Combiner auszuwerten, können sie beispielsweise auch gespeichert werden.

#include <boost/signal.hpp> 
#include <iostream> 
#include <vector> 
#include <algorithm> 

int func1() 
{ 
  return 1; 
} 

int func2() 
{ 
  return 2; 
} 

template <typename T> 
struct min_element 
{ 
  typedef T result_type; 

  template <typename InputIterator> 
  T operator()(InputIterator first, InputIterator last) const 
  { 
    return T(first, last); 
  } 
}; 

int main() 
{ 
  boost::signal<int (), min_element<std::vector<int> > > s; 
  s.connect(func1); 
  s.connect(func2); 
  std::vector<int> v = s(); 
  std::cout << *std::min_element(v.begin(), v.end()) << std::endl; 
} 

Im obigen Programm werden Rückgabewerte in einem Vektor gespeichert, der dann von s() zurückgegeben wird.


4.3 Verbindungen

Sie können Funktionen mit Hilfe der Methoden connect() und disconnect(), die von boost::signal angeboten werden, verwalten. Da connect() jedoch einen Rückgabewert vom Typ boost::signals::connection besitzt, geht dies auch anderweitig.

#include <boost/signal.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  boost::signals::connection c = s.connect(func); 
  s(); 
  c.disconnect(); 
} 

Anstatt für boost::signal disconnect() aufzurufen und als Parameter einen Funktionszeiger zu übergeben, kann auch für boost::signals::connection disconnect() aufgerufen werden, ohne dass der entsprechende Funktionszeiger übergeben werden muss.

Neben disconnect() bietet die Klasse boost::signals::connection Methoden wie block() und unblock() an.

#include <boost/signal.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  boost::signals::connection c = s.connect(func); 
  c.block(); 
  s(); 
  c.unblock(); 
  s(); 
} 

Im obigen Programm wird func() genau einmal ausgeführt. Obwohl das Signal s zweimal ausgelöst wird, findet beim ersten Mal kein Aufruf der Funktion func() statt. Der Grund ist, dass die Verbindung c mit block() geblockt wurde. Weil vor dem zweiten Signal unblock() aufgerufen wurde, wird func() an dieser Stelle ausgeführt.

Neben boost::signals::connection steht eine Klasse boost::signals::scoped_connection zur Verfügung, die im Destruktor automatisch die Verbindung löst.

#include <boost/signal.hpp> 
#include <iostream> 

void func() 
{ 
  std::cout << "Hello, world!" << std::endl; 
} 

int main() 
{ 
  boost::signal<void ()> s; 
  { 
    boost::signals::scoped_connection c = s.connect(func); 
  } 
  s(); 
} 

Da im obigen Programm das Verbindungsobjekt c zerstört wird, bevor das Signal ausgelöst wird, findet kein Aufruf von func() statt.

boost::signals::scoped_connection ist von boost::signals::connection abgeleitet, so dass die gleichen Methoden zur Verfügung stehen. Der einzige Unterschied ist, dass die Verbindung im Destruktor von boost::signals::scoped_connection automatisch gelöst wird.

Während die Klasse boost::signals::scoped_connection es erleichtert, Verbindungen automatisch zu lösen, müssen Objekte dieses Typs dennoch verwaltet werden. Es wäre schön, wenn sich auch in anderen Fällen Verbindungen automatisch lösen würden, ohne dass Verbindungsobjekte verwaltet werden müssen. Sehen Sie sich dazu folgendes Beispiel an.

#include <boost/signal.hpp> 
#include <boost/bind.hpp> 
#include <iostream> 
#include <memory> 

class world 
{ 
  public: 
    void hello() const 
    { 
      std::cout << "Hello, world!" << std::endl; 
    } 
}; 

int main() 
{ 
  boost::signal<void ()> s; 
  { 
    std::auto_ptr<world> w(new world()); 
    s.connect(boost::bind(&world::hello, w.get())); 
  } 
  std::cout << s.num_slots() << std::endl; 
  s(); 
} 

Im obigen Programm wird eine Methode eines Objekts mit einem Signal verbunden. Dies geschieht mit Hilfe von Boost.Bind. Das Problem im obigen Programm ist, dass das entsprechende Objekt zerstört wird, bevor das Signal ausgelöst wird. Da nicht das Objekt w an boost::bind() übergeben wurde, sondern ein Zeiger auf w, wird beim Aufruf von s() über einen Zeiger auf ein Objekt zugegriffen, das gar nicht mehr existiert.

Es ist nun möglich, das Programm so anzupassen, dass die Verbindung automatisch gelöst wird, wenn das Objekt w zerstört wird.

#include <boost/signal.hpp> 
#include <boost/bind.hpp> 
#include <iostream> 
#include <memory> 

class world : 
  public boost::signals::trackable 
{ 
  public: 
    void hello() const 
    { 
      std::cout << "Hello, world!" << std::endl; 
    } 
}; 

int main() 
{ 
  boost::signal<void ()> s; 
  { 
    std::auto_ptr<world> w(new world()); 
    s.connect(boost::bind(&world::hello, w.get())); 
  } 
  std::cout << s.num_slots() << std::endl; 
  s(); 
} 

Wenn Sie obiges Programm ausführen, stellen Sie fest, dass num_slots() 0 zurückgibt. Es wird demnach beim Auslösen des Signals nicht versucht, eine Methode für ein Objekt aufzurufen, das bereits zerstört wurde. Die einzige Änderung, die dazu im Programm notwendig war: Die Klasse world muss von der Klasse boost::signals::trackable abgeleitet werden. Wann immer Sie Methoden mit Signalen verbinden möchten und dabei nicht Objekte als Kopie, sondern Zeiger auf diese übergeben, kann die Klasse boost::signals::trackable die Verwaltung der Verbindungen wesentlich erleichtern.


4.4 Aufgaben

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Erstellen Sie ein Programm, in dem Sie eine Klasse button definieren. Diese Klasse soll eine anklickbare Schaltfäche in einer grafischen Benutzeroberfläche repräsentieren. Fügen Sie dieser Klasse die Methoden add_handler() und remove_handler() hinzu, denen ein Funktionsname übergeben werden können soll. Wenn eine Methode click() aufgerufen wird, sollen die registrierten Funktionen nacheinander aufgerufen werden. Erstellen Sie eine Instanz der Klasse button und testen Sie sie, indem Sie in einem Ereignisverarbeiter eine Meldung auf die Standardausgabe ausgeben. Rufen Sie click() auf, um einen Mausklick auf die Schaltfläche zu simulieren.